A canonical example of where "evil merge" happens in real life (and is a good thing) is to adjust for semantic conflicts. It almost never have anything to do with textual conflicts.
Imagine you and your friend start with the same codebase where a function f() takes no parameter, and has two existing call-sites.
You decide to update the function to take a parameter, and adjust both existing call-sites to pass one argument to the function. Your friend in the meantime added a new call-site that calls f() still without an argument. Then you merge.
It is very likely that you won't see any textual conflict. Your friend added some code to block of lines you did not touch while you two were forked. However, the end result is now wrong. Your updated f() expects one parameter, while the new call-site your friend added does not pass any argument to it.
In such a case, you would fix the new call-site your friend added to pass an appropriate argument and record that as part of your merge.
Consider the line that has that new call-site you just fixed. It certainly did not exist in your version (it came from your friend'd code), but it is not exactly what your friend wrote, either (it did not pass any argument). That makes the merge an "evil merge".
With "git log -c/--cc", such a line will show with double-plus in the multi-way patch output to show that "this did not exist in either parent".