Well first off, if you have something that suits bytes well (for instance, I have compressed indexes that are base128 delta encoded values, so a byte* is good enough for that) - just use the normal malloc routines and call it a day.
For allocating managed objects on unmanaged heaps, you just need to read a bit about CLR internals. The object pointer (so the value of s in "var s = "hello") points to the start of the object data (IIRC). There's a few words in front of that that provide the object type, as well as the sync block (kitchen sync). Arrays have their own little syntax, where they have a type indicator as well as the length.
You can just manually copy those by pinning an arbitrary object and grabbing a few bytes before it. Then you can write those values to arbitrary memory locations, and use that memory address as the value to put back into a managed object reference.
For APIs that need strings, arrays, or other simple heap-allocated objects, but are pure and just return (like, say, String.Split takes an array to hold splitter values), you might get some large wins by stack allocating them.
The parts of my app that were GC managed, the optimization mainly went into reducing the number of objects allocated - at high request rates, even a few measly objects here and there can easily add GC pressure and suck up CPU. (Although, I will say the CLR GC does pretty well with short lived objects.)
You can also use the freedom of memory access to do things like mutate a string, or to change the type of an array (say, from a byte[] to a long[]).
Another common trick is allocating large arrays of structs, then passing ints that "point" to the struct in question. This a: locks the structs in memory and avoids GC if the entire array is over 85K (it goes on the Large Object Heap), and second, you're only passing a single int param, instead of a struct, which can be slow for larger structs.