I think Marshal doesn't have a Copy(IntPtr, UInt16[], Int32, Int32) overload because of CLS compliance (a tenet of CLS compliance is that the integer types SByte, UInt16, UInt32, and UInt64 are not exposed to consumers because they aren't supported by the broader gamut of CLR-compatible languages - and it's hardly rare: even in 2022 Java still doesn't have unsigned types.
With marshalling integer arrays, all that matters is the size of the elements, not their signed-ness - this also goes for Array.BlockCopy - so I would either use the Byte or Int16 overloads and do my own casting afterwards, or I would use pointers if unsafe is allowed.
Update for .NET Core and later:
In .NET 6 (and likely .NET Core too) the Marshal.Copy method's implementation is just this:
private static unsafe void CopyToManaged<T>(IntPtr source, T[] destination, int startIndex, int length)
{
    if (source == IntPtr.Zero) throw new ArgumentNullException(nameof(source));
    if (destination is null) throw new ArgumentNullException(nameof(destination));
    if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
    if (length < 0) throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum);
    void*   sourcePtr = (void*)source;
    Span<T> srcSpan   = new Span<T>(sourcePtr, length);
    Span<T> destSpan  = new Span<T>(destination, startIndex, length);
    srcSpan.CopyTo(destSpan);
}
So you could call-into this method using reflection, or just copy+paste it yourself.
If you can't use unsafe or .NET Core... try type-punning:
I wrote and tested this just now and it runs without issues on .NET Framework 4.8, .NET Core 3.1, and .NET 6, phew:
...but don't go using type-punning everywhere, it's only safe or workable in certain situations like these:
void Main()
{
    // i.e. `calloc( 0x1000, 2 )`
    IntPtr nativeArray = Marshal.AllocHGlobal( cb: 0x1000 * sizeof(UInt16) ); // 2KiB == 2048 bytes == 1024 shorts.
    {
        // TODO: Zero-out `nativeArray`, and all memory returned from `AllocHGlobal`, but it's fiddly, see here: https://stackoverflow.com/questions/1486999/how-to-zero-out-memory-allocated-by-marshal-allochglobal
    }
    
    UInt16[] u16Array = new UInt16[ 0x1000 ];
    
    // Fill the array with incrementing numbers so we can ensure it's copied correctly:
    for( Int32 i = 0; i < u16Array.Length; i++ ) u16Array[i] = (UInt16)(( Int16.MaxValue - 500 ) + i); // `( 32767 - 500 ) + n` ==> [ 32267, 32267, ..., 33267 ] which is between Int16.MaxValue and UInt16.MaxValue.
    
    // Very-self documenting code:
    ThisStructPunsTypes s = new ThisStructPunsTypes( u16Array );
    Int16[] staticTypeInt16RealTypeUInt16 = s.AsInt16;
    
    // Proof of Magic:
    Debug.Assert( Object.ReferenceEquals( u16Array, staticTypeInt16RealTypeUInt16 ) == true, message: "These two array references, of different types, reference the same single underlying UInt16[]." );
    
    // Copy values from `u16Array` into `nativeArray` using type-punning:
    {
        Marshal.Copy( source: s.AsInt16, startIndex: 0, destination: nativeArray, length: 0x1000 );
    }
    
    // Get the values back out directly into a new (separate) Int16[] buffer:
    Int16[] outputInt16Array;
    {
        outputInt16Array = new Int16[ 0x1000 ];
        Marshal.Copy( source: nativeArray, destination: outputInt16Array, startIndex: 0, length: 0x1000 );
    }
    
    // Get the values back out directly into a new (separate) UInt16[] buffer via type-punning:
    UInt16[] outputUInt16Array;
    {
        outputUInt16Array = new UInt16[ 0x1000 ];
        
        ThisStructPunsTypes again = new ThisStructPunsTypes( outputUInt16Array );
        
        Int16[] typePunAgain = again.AsInt16;
        
        Marshal.Copy( source: nativeArray, destination: typePunAgain, startIndex: 0, length: 0x1000 );
    }
    
    
    Debug.Assert( Object.ReferenceEquals( u16Array, outputInt16Array ) != true, message: "These are two separate array objects." );
    Debug.Assert( Object.ReferenceEquals( outputInt16Array, outputUInt16Array ) != true, message: "These are two separate array objects." );
    // Observe the values are copied fine from a UInt16 array into native memory, then into a separate and new Int16 array:
    Debug.Assert( outputInt16Array[   0] ==  32267 );
    Debug.Assert( outputInt16Array[   1] ==  32268 );
    Debug.Assert( outputInt16Array[   2] ==  32269 );
    Debug.Assert( outputInt16Array[ 499] ==  32766 );
    Debug.Assert( outputInt16Array[ 500] ==  32767 );
    Debug.Assert( outputInt16Array[ 501] == -32768 );
    Debug.Assert( outputInt16Array[ 502] == -32767 );
    Debug.Assert( outputInt16Array[4095] == -29174 );
    Debug.Assert( outputInt16Array.Length == 4096 );
    
    // Observe the values are copied fine from a UInt16 array into native memory, then into a separate and new UInt16 array:
    Debug.Assert( outputUInt16Array[   0] ==  32267 );
    Debug.Assert( outputUInt16Array[   1] ==  32268 );
    Debug.Assert( outputUInt16Array[   2] ==  32269 );
    Debug.Assert( outputUInt16Array[ 499] ==  32766 );
    Debug.Assert( outputUInt16Array[ 500] ==  32767 );
    Debug.Assert( outputUInt16Array[ 501] ==  32768 );
    Debug.Assert( outputUInt16Array[ 502] ==  32769 );
    Debug.Assert( outputUInt16Array[4095] ==  36362 );
    Debug.Assert( outputUInt16Array.Length == 4096 );
}
[StructLayout( LayoutKind.Explicit )]
readonly struct ThisStructPunsTypes
{
    public ThisStructPunsTypes( UInt16[] uint16Array )
    {
        this.AsInt16  = default!;
        this.AsUInt16 = uint16Array ?? throw new ArgumentNullException(nameof(uint16Array));
    }
    
    [FieldOffset( 0 )]
    public readonly Int16[]  AsInt16;
    
    [FieldOffset( 0 )]
    public readonly UInt16[] AsUInt16;
}
Slow, but safe, way:
If you want something that just works and assuming you're not bothered about having a temporary second copy of the data then this would work:
static UInt16[] GetUInt16Array( IntPtr unmanagedArray, Int32 count )
{
    if( unmanagedArray == default ) throw new ArgumentException( ... );
    if( count < 0 ) throw new ArgumentOutOfRangeException( ... );
    //
    Int16[] signedInt16Array = new Int16[ count ];
    Marshal.Copy( source: unmanagedArray, destination: signedInt16Array, startIndex: 0, length: count );
    Int16[] outputUnsignedInt16Array = new UInt16[ count ];
    Array.Copy( sourceArray: signedInt16Array, destinationArray: outputUnsignedInt16Array, length: count );
    return outputUnsignedInt16Array;
}