So I'm mostly talking about using unsafe C# as a way of doing things that the runtime otherwise prevents you from doing - i.e. you're in a C# codebase and want to implement some data structure or algorithm that doesn't sit welll with the GC/runtime. As an example, perhaps it relies on many contiguous buffers larger than the LOH threshold, or uses many medium-lifespan objects (a common usage pattern that GC, at least of the .NET variety, has no better answer to than "try not to do that" but which arena allocation handles with essentially no overhead).
I'd distinguish between those use cases and ones where you're using unsafe C# to interface with external unmanaged code (I've done both). Such interop-like use cases are also rather hairier than writing C against those APIs because with C at least you can generally consume a header file and have some confidence that your compiler understands the binary interface you'll be interacting with at runtime, whereas in C# the burden of trying to ensure that the data types and signatures are correct against the actual binary loaded is on you, and it can be a heavy one especially if you want your code to work cross platform. (Yes, MS have come up with things like C++/CLI that should help, but have shown no commitment to them. If you want things to reliably work across platforms and in the future you're basically stuck with P/Invoke and unsafe.)
But the dangers are worse for the former kind of code. Here the worst risks arise around the interactions between the unsafe code and the safe code. Typically in this sort of situation you want to wrap your nasty unsafe code in a nice, safe API. You want to make it so that ordinary C# callers can't make mistakes with your managed API that would cause crashes, leaks, etc. In fact, it's more than "want". You really have to. It's C# and people need to be able to program in it like it's C#, without expecting that a missed "using" somewhere is going to cause a segfault. And that can be really hard to do. The crux of the problem is that the GC wants to manage the lifetimes of the managed objects and offers no way to hook into those lifetimes other than finalizers, with their well known limitations/downsides or disposal which you can't rely on callers to invoke. If you create/manipulate some unmanaged resource and wrap it in a managed API there are lots of fun little gotchas to run into, such as the fact that an object can be garbage collected while a method on it is executing. [1]
I believe it's for this reason that interesting unmanaged data structures written in unsafe C# and exposed as safe-to-use C# types aren't really a thing in the .NET world. It really is very hard indeed to do it safely and efficiently. Unsafe is more commonly used for interop glue, where it's better suited (but still a bit risky).
The need to do things like this has led to improvements such as spans, but I'm not a fan. A ref struct is a horribly, arbitrarily limited thing (it can basically only exist on the stack) and I found programming with Spans an exercise in frustration as a result. For one thing, the moment you find you need a span of spans you'll find yourself reaching for those grubby raw pointers again. And at best they really just give you bounds checking. They don't help at all with the hard problem: resource management (and neither does Memory).
So the comparison with C - what do I mean by "more dangerous"? Well, it's a subjective thing, of course. I don't really mean there are more opportunities to screw up. What I mean is that when I was writing this stuff I had to work harder, think harder and move slower not to screw up. The pitfalls were less obvious, there was less prior art, fewer established practices and a general feeling that I was going against the grain. YMMV, but it's not something I'm going to miss.
[1] https://devblogs.microsoft.com/oldnewthing/20100810-00/?p=13...