It all revolves around the Marshal class in System.Runtime.InteropServices. To allocate a chunk of unmanged memory, do:
IntPtr myMemory = System.Runtime.InteropServices.Marshal.AllocHGlobal(numberOfBytes);
The 'IntPtr' is C#'s way of giving you a pointer reference. You can't do anything directly with it, but you can pass it around. The Marshall.Copy() function gives you a bunch of ways to copy chunks of various types of managed data to and from unmanaged data you have IntPtr references to.
What if you want to allocate a multi-dimensional unmanaged array? Things get a bit more complicated, but it can be done. Here is an example of allocating a 10 by 100 array of floats:
int dimension1 = 10;
int dimension2 = 100;
IntPtr arrayPtr = Marshal.AllocHGlobal(dimension1 * Marshal.SizeOf(typeof(IntPtr)));
IntPtr dimension2Pointers = new IntPtr[dimension1];
for (int pos = 0; pos < dimension1; pos++)
dimension2Pointers[pos] = Marshal.AllocHGlobal(dimension2 * sizeof(float));
Marshal.Copy(dimension2Pointers, 0, arrayPtr, dimension1);
With a bit more work, you could probably avoid having to use the managed IntPtr array. In my case, though, I wanted managed pointers to both the full two-dimensional array as well as each of the individual one-dimensional arrays.
The use of Marshal.SizeOf() instead of sizeof() is because sizeof(IntPtr) is indeterminate unless you are in unsafe code.
Don't forget that this is all unmanaged memory, so you are responsible for freeing it using the corresponding Marshall.FreeHGlobal() call.