GCHandle.AddOrPinnedObject is for passing the address of the object through unmanaged code, and it requires the object is pinned.
That address could then be used for marshalling to pass the object into a native code function, and the native code could manipulate the object. The managed object must have restricted type-layout.
It’s an instance method on pinned GCHandle that returns the real address of the pinned object. Pinning the object prevents the GC from moving it, so the object’s address is stable. This has to return an IntPtr (or perhaps a UIntPtr) because it’s returning an object address.
GCHandle.ToIntPtr is for passing the GC handle through unmanaged code, and it does not require the object is pinned. This lets unmanaged code hold GC roots in the managed heap (see gcroot<T>).
It returns a bit encoding for the GC handle. There’s an extra level of indirection here because it’s going through the handle-table, but the win is that the object is not pinned. The only thing you can do with this IntPtr is pass it back to GCHandle.FromIntPtr to get back the GC handle.
GCHandles don’t have to be round-tripped through an IntPtr. You could imagine an alternative implementation where they’re round-tripped through an int32 (like most OS handles). IntPtr just gives more flexibility.
For more info, Chris Lyon has a great blog entry on GCHandles.